﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Drawing;
using System.Drawing.Imaging;
using System.Drawing.Drawing2D; // InterpolationMode

namespace ImageProcessingLibrary
{

    public unsafe class ImageProcessor
    {
        #region Fields
        private Bitmap processedBitmap;
        internal BitmapData processedData;
        private Boolean isLocked;
        private Boolean isGrayScale;
        private Boolean isBinarized;
        private int[,] integralImage;

        private Boolean isComponentsConnected;
        private ConnectedComponent connectedComponents;
        #endregion

        #region Constants
        internal const int PIXEL_SIZE = 3;
        const int DEFAULT_THRESHOLD1 = 127;
        const int DEFAULT_THRESHOLD2 = 255;
        const int RUNNING_AVERAGE_BETA = 2;

        const int CB_LOW = 102;
        const int CB_HIGH = 126;
        const int CR_LOW = 140;
        const int CR_HIGH = 162;
        #endregion

        #region Private methods
        #endregion

        #region Public methods
        public ImageProcessor(Bitmap bitmap)
        {
            processedBitmap = new Bitmap(bitmap);
            isLocked = false;
            isGrayScale = false;
            isBinarized = false;
            isComponentsConnected = false;
        }

        public void Lock()
        {
            if (!isLocked)
            {
               processedData = processedBitmap.LockBits(
                       new Rectangle(0, 0, processedBitmap.Width, processedBitmap.Height),
                       ImageLockMode.ReadWrite, PixelFormat.Format24bppRgb); //Changed to ReadWrite for multiple operations..
               isLocked = true;
            }
        }

        public void UnLock()
        {
            if (isLocked)
            {
                processedBitmap.UnlockBits(processedData);
                isLocked = false;
            }
        }

        public ImageProcessor Copy()
        {
            ImageProcessor copy = new ImageProcessor(this.ProcessedBitmap); // The constructor of ImageProcessor will copy the bitmap
            return copy;
        }

        public void Invert()
        {
            for (int i = 0; i < processedBitmap.Height; i++)
            {
                byte* pRow = (byte*)processedData.Scan0 + (i * processedData.Stride);
                for (int j = 0; j < processedData.Width; j++)
                {
                    byte rColor = (byte)(pRow[j * PIXEL_SIZE + 2]);
                    byte gColor = (byte)(pRow[j * PIXEL_SIZE + 1]);
                    byte bColor = (byte)(pRow[j * PIXEL_SIZE]);
                    byte maxValue = 255;
                    pRow[j * PIXEL_SIZE + 2] = (byte)(maxValue - rColor);
                    pRow[j * PIXEL_SIZE + 1] = (byte)(maxValue - gColor);
                    pRow[j * PIXEL_SIZE] = (byte)(maxValue - gColor);
                }
            }
        }

        public void ConvertToGrayScale(double rFraction, double gFraction, double bFraction)
        {
            for (int i = 0; i < processedBitmap.Height; i++)
            {
                byte* pRow = (byte*)processedData.Scan0 + (i * processedData.Stride);
                for (int j = 0; j < processedData.Width; j++)
                {
                    byte grayColor =
                      (byte)((pRow[j * PIXEL_SIZE] * bFraction) +
                             (pRow[j * PIXEL_SIZE + 1] * gFraction) +
                             (pRow[j * PIXEL_SIZE + 2] * rFraction));
                    pRow[j * PIXEL_SIZE] = grayColor;     //Note! B
                    pRow[j * PIXEL_SIZE + 1] = grayColor; //Note! G
                    pRow[j * PIXEL_SIZE + 2] = grayColor; //Note! R
                }
            }
            isGrayScale = true;
        }

        public void ConvertToStandardGrayScale()
        {
            ConvertToGrayScale(0.30, 0.59, 0.11);
        }

        public void BinarizeGray(int threshold1, int threshold2)
        {
            if (!isGrayScale)
            {
                ConvertToStandardGrayScale();
            }
            for (int i = 0; i < processedBitmap.Height; i++)
            {
                byte* pRow = (byte*)processedData.Scan0 + (i * processedData.Stride);
                for (int j = 0; j < processedData.Width; j++)
                {
                    byte pixelColor = (byte)(pRow[j * PIXEL_SIZE]);  // Any channel OK, since they are all equal..
                    if ((pixelColor < threshold1) || (pixelColor > threshold2))
                    {
                        pixelColor = 0;
                    }
                    else
                    {
                        pixelColor = 255;
                    }
                    pRow[j * PIXEL_SIZE] = pixelColor;
                    pRow[j * PIXEL_SIZE + 1] = pixelColor;
                    pRow[j * PIXEL_SIZE + 2] = pixelColor;
                }
            }
            isBinarized = true;
        }

        public void GenerateIntegralImage()
        {
            // Note: This method assumes that the image has been binarized.
            integralImage = new int[processedBitmap.Height, processedBitmap.Width];
            for (int i = 0; i < processedBitmap.Height; i++)
            {
                byte* pRow = (byte*)processedData.Scan0 + (i * processedData.Stride);
                for (int j = 0; j < processedBitmap.Width; j++)
                {
                    byte color = pRow[j * PIXEL_SIZE];
                    if (color == 0)
                    {
                        integralImage[i, j] = 0;
                    }
                    else
                    {
                        integralImage[i, j] = 1;
                    }
                    if (i > 0)
                    {
                        integralImage[i, j] += integralImage[i - 1, j];
                    }
                    if (j > 0)
                    {
                        integralImage[i, j] += integralImage[i, j - 1];
                    }
                    if ((i > 0) && (j > 0))
                    {
                        integralImage[i, j] -= integralImage[i - 1, j - 1];
                    }
                }
            }
        }

        public void RemoveSinglePixels()
        {
            // Assumes that the image has been binarized. 
            // If not, the image is binarized using default thresholds.
            if (!isBinarized)
            {
                BinarizeGray(DEFAULT_THRESHOLD1, DEFAULT_THRESHOLD2);
            }
            Bitmap cleanedBitmap = new Bitmap(processedBitmap.Width, processedBitmap.Height);
            BitmapData cleanedData = cleanedBitmap.LockBits(
               new Rectangle(0, 0, processedBitmap.Width, processedBitmap.Height),
               ImageLockMode.ReadWrite, PixelFormat.Format24bppRgb);
            for (int i = 1; i < processedBitmap.Height - 1; i++)  // Should perhaps introduce "EdgeIgnoreSize"
            {
                byte* pRowAbove = (byte*)processedData.Scan0 + ((i - 1) * processedData.Stride);
                byte* pRow = (byte*)processedData.Scan0 + (i * processedData.Stride);
                byte* pRowBelow = (byte*)processedData.Scan0 + ((i + 1) * processedData.Stride);
                byte* cRow = (byte*)cleanedData.Scan0 + (i * cleanedData.Stride);
                for (int j = 1; j < processedBitmap.Width - 1; j++)
                {
                    cRow[j * PIXEL_SIZE] = pRow[j * PIXEL_SIZE];
                    if (pRow[j * PIXEL_SIZE] != 0)
                    {
                        byte c11 = pRowAbove[(j - 1) * PIXEL_SIZE];
                        byte c12 = pRowAbove[j * PIXEL_SIZE];
                        byte c13 = pRowAbove[(j + 1) * PIXEL_SIZE];
                        if ((c11 == 0) && (c12 == 0) && (c13 == 0))
                        {
                            byte c21 = pRow[(j - 1) * PIXEL_SIZE];
                            byte c23 = pRow[(j + 1) * PIXEL_SIZE];
                            if ((c21 == 0) && (c23 == 0))
                            {
                                byte c31 = pRowBelow[(j - 1) * PIXEL_SIZE];
                                byte c32 = pRowBelow[j * PIXEL_SIZE];
                                byte c33 = pRowBelow[(j + 1) * PIXEL_SIZE];
                                if ((c31 == 0) && (c32 == 0) && (c33 == 0))
                                {
                                    cRow[j * PIXEL_SIZE] = 0;
                                }
                            }
                        }
                    }
                }
            }
            for (int i = 1; i < processedBitmap.Height - 1; i++)
            {
                byte* pRow = (byte*)processedData.Scan0 + (i * processedData.Stride);
                byte* cRow = (byte*)cleanedData.Scan0 + (i * cleanedData.Stride);
                for (int j = 1; j < processedBitmap.Width - 1; j++)
                {
                    pRow[j * PIXEL_SIZE] = cRow[j * PIXEL_SIZE];
                    pRow[j * PIXEL_SIZE + 1] = cRow[j * PIXEL_SIZE];
                    pRow[j * PIXEL_SIZE + 2] = cRow[j * PIXEL_SIZE];
                }
            }
            cleanedBitmap.UnlockBits(cleanedData);
        }

        public Histogram GenerateHistogram(int channelIndex)
        {
            Histogram histogram = new Histogram(256);
            for (int i = 0; i < processedBitmap.Height; i++)
            {
                byte* pRow = (byte*)processedData.Scan0 + (i * processedData.Stride);
                for (int j = 0; j < processedData.Width; j++)
                {
                        byte pixelColor = (byte)(pRow[j * PIXEL_SIZE + (3 - channelIndex)]); // 1 = B, 2 = G, 3 = R.
                        histogram[pixelColor] += 1;
                }
            }
            return histogram;
        }

        public void EqualizeGrayHistogram() 
        {
            // Assumes that the image has been converted to gray scale.
            // If not, the image is converted to standard gray scale.
            if (!isGrayScale)
            {
                ConvertToStandardGrayScale();
            }
            Histogram histogram = GenerateHistogram(1); // Arbitrary channel..
            histogram.MakeCumulative();
            for (int i = 0; i < processedData.Height; i++)
            {
                byte* pRow = (byte*)processedData.Scan0 + (i * processedData.Stride);
                int width = processedBitmap.Width;
                int height = processedBitmap.Height;
                for (int j = 0; j < processedData.Width; j++)
                {
                    int grayColor = pRow[j * PIXEL_SIZE];
                    byte newColor = (byte)Math.Round(255.0 * histogram[grayColor] / (width * height));
                    pRow[j * PIXEL_SIZE] = newColor;     //Note! B
                    pRow[j * PIXEL_SIZE + 1] = newColor;
                    pRow[j * PIXEL_SIZE + 2] = newColor;
                }
            }
        }
       
        public void ConvolveGray(List<List<double>> convolutionMatrix)
        {
            if (!isGrayScale)
            {
                ConvertToStandardGrayScale();
            }
            Bitmap convolvedBitmap = new Bitmap(processedBitmap.Width, processedBitmap.Height);
            BitmapData convolvedData = convolvedBitmap.LockBits(
               new Rectangle(0, 0, processedBitmap.Width, processedBitmap.Height),
               ImageLockMode.ReadWrite, PixelFormat.Format24bppRgb);
            int halfSizeX = (int)Math.Truncate((convolutionMatrix[0].Count - 1) / 2.0);
            int halfSizeY = (int)Math.Truncate((convolutionMatrix.Count - 1) / 2.0);
            for (int i = halfSizeY; i < processedData.Height - halfSizeY; i++)
            {
                for (int j = halfSizeX; j < processedData.Width - halfSizeX; j++)
                {
                    // Compute the modified value using matrix multiplication:
                    double tmpConvolvedValue = 0;
                    for (int k = i - halfSizeY; k <= i + halfSizeY; k++)
                    {
                        int kCount = k - i + halfSizeY; // y-index of convolution matrix element.
                        byte* pRow = (byte*)processedData.Scan0 + k * processedData.Stride;
                        for (int m = j - halfSizeX; m <= j + halfSizeX; m++)
                        {
                            int mCount = m - j + halfSizeX; // x-index of convolution matrix element.
                            tmpConvolvedValue +=
                                pRow[m * PIXEL_SIZE] * convolutionMatrix[kCount][mCount]; // CHECK!!!
                        }
                    }
                    byte convolvedValue = (byte)Math.Round(tmpConvolvedValue);
                    if (tmpConvolvedValue < 0)
                    {
                        convolvedValue = 0;
                    }
                    else if (tmpConvolvedValue > 255)
                    {
                        convolvedValue = 255;
                    }
                    byte* cRow = (byte*)convolvedData.Scan0 + i * convolvedData.Stride;
                    cRow[j * PIXEL_SIZE] = convolvedValue;
                    cRow[j * PIXEL_SIZE + 1] = convolvedValue;
                    cRow[j * PIXEL_SIZE + 2] = convolvedValue;
                }
            }

            // Copy the temporary (convolved) bitmap onto the processed bitmap:
            for (int i = halfSizeY; i < processedBitmap.Height - halfSizeY; i++)
            {
                byte* pRow = (byte*)processedData.Scan0 + (i * processedData.Stride);
                byte* cRow = (byte*)convolvedData.Scan0 + (i * convolvedData.Stride);
                for (int j = halfSizeX; j < processedBitmap.Width - halfSizeX; j++)
                {
                        pRow[j * PIXEL_SIZE] = cRow[j * PIXEL_SIZE];
                        pRow[j * PIXEL_SIZE + 1] = cRow[j * PIXEL_SIZE + 1];
                        pRow[j * PIXEL_SIZE + 2] = cRow[j * PIXEL_SIZE + 2];
                }
            }
            convolvedBitmap.UnlockBits(convolvedData);
        }

        public Bitmap GetSkinPixelsRGB()
        {
            Bitmap skinBitmap = new Bitmap(processedBitmap.Width, processedBitmap.Height);
            BitmapData skinData = skinBitmap.LockBits(
                   new Rectangle(0, 0, processedBitmap.Width, processedBitmap.Height),
                   ImageLockMode.ReadWrite, PixelFormat.Format24bppRgb);
            for (int i = 0; i < processedBitmap.Height; i++)
            {
                byte* pRow = (byte*)processedData.Scan0 + (i * processedData.Stride); // YCbCr format
                byte* sRow = (byte*)skinData.Scan0 + (i * skinData.Stride); // Y component as RGB
                for (int j = 0; j < processedData.Width; j++)
                {
                    byte b = pRow[j * PIXEL_SIZE];
                    byte g = pRow[j * PIXEL_SIZE + 1];
                    byte r = pRow[j * PIXEL_SIZE + 2];

                    byte maxColor = Math.Max(r, Math.Max(g, b));
                    byte minColor = Math.Min(r, Math.Min(g, b));

                    if ( ((r > 95) && (g > 40) && (b > 20)) & ((maxColor-minColor) > 15) &&
                         ((int)Math.Abs(r-g) > 15) && ((int)Math.Abs(r-g) < 75) && (r > g) && (r > b))
                    {
                        sRow[j * PIXEL_SIZE] = 255;     //B
                        sRow[j * PIXEL_SIZE + 1] = 255; //G
                        sRow[j * PIXEL_SIZE + 2] = 255; //R
                    }
                    else
                    {
                        sRow[j * PIXEL_SIZE] = 0;
                        sRow[j * PIXEL_SIZE + 1] = 0;
                        sRow[j * PIXEL_SIZE + 2] = 0;
                    }
                }
            }
            skinBitmap.UnlockBits(skinData);
            return skinBitmap;
        }

        #endregion

        #region Public New methods
        public ImageProcessor GetSkinPixelsYCbCr()
        {
            ImageProcessor skinBitmap = new ImageProcessor(new Bitmap(processedBitmap.Width, processedBitmap.Height));
            skinBitmap.Lock();

            for (int i = 0; i < processedBitmap.Height; i++)
            {
                byte* pRow = (byte*)processedData.Scan0 + (i * processedData.Stride);
                byte* sRow = (byte*)skinBitmap.processedData.Scan0 + (i * skinBitmap.processedData.Stride);
                for (int j = 0; j < processedData.Width; j++)
                {
                    byte b = pRow[j * PIXEL_SIZE];
                    byte g = pRow[j * PIXEL_SIZE + 1];
                    byte r = pRow[j * PIXEL_SIZE + 2];

                    // Color covertion as defined for SDTV-standard ITU-R BT.601
                    int cb = 128 - ((38 * r) + (74 * g) - (112 * b)) / 256;
                    int cr = 128 + ((112 * r) - (94 * g) - (18 * b)) / 256;

                    if (cb > CB_LOW && cb < CB_HIGH && cr > CR_LOW && cr < CR_HIGH)
                    {
                        sRow[j * PIXEL_SIZE] = 255;     //B
                        sRow[j * PIXEL_SIZE + 1] = 255; //G
                        sRow[j * PIXEL_SIZE + 2] = 255; //R
                    }
                    else
                    {
                        sRow[j * PIXEL_SIZE] = 0;
                        sRow[j * PIXEL_SIZE + 1] = 0;
                        sRow[j * PIXEL_SIZE + 2] = 0;
                    }
                }
            }

            skinBitmap.isGrayScale = true;
            skinBitmap.isBinarized = true;

            skinBitmap.UnLock();
            return skinBitmap;
        }

        // Preferably, remove single pixels before calling.
        public void ExtractConnectedComponents()
        {
            if (!isGrayScale)
            {
                ConvertToStandardGrayScale();
            }
            if (!isBinarized)
            {
                BinarizeGray(DEFAULT_THRESHOLD1, DEFAULT_THRESHOLD2);
            }

            this.connectedComponents = new ConnectedComponent(this);
            this.isComponentsConnected = true;
        }

        public void RemoveSmallComponents(int threshold)
        {
            if (!this.isComponentsConnected)
            {
                this.ExtractConnectedComponents();
            }

            this.connectedComponents.RemoveSmallComponents(threshold);
        }

        public void RemoveNonFaces(double widthVsHeightLow, double widthVsHeightHigh)
        {
            if (!this.isComponentsConnected)
            {
                this.ExtractConnectedComponents();
            }

            this.connectedComponents.RemoveNonFaces(widthVsHeightLow, widthVsHeightHigh);
        }

        public void FilterOutConnectedComponents(ImageProcessor image)
        {
            if (!this.isComponentsConnected)
            {
                this.ExtractConnectedComponents();
            }

            this.connectedComponents.KeepOnlyConnectedPixels(image);
        }

        public bool ConnectedRegionsExist()
        {
            if (!this.isComponentsConnected)
            {
                this.ExtractConnectedComponents();
            }

            return this.connectedComponents.NumberOfComponents > 0;
        }
        #endregion

        #region Public properties
        public Bitmap ProcessedBitmap
        {
            get { return processedBitmap; }
        }

        public int[,] IntegralImage
        {
            get { return integralImage; }
            set { integralImage = value; }
        }
        #endregion

    }
}
